package com.excilys.formation.project.repository.impl;

import static org.springframework.util.Assert.noNullElements;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.joda.time.Interval;
import org.springframework.stereotype.Repository;

import com.excilys.formation.project.entities.Operation;
import com.excilys.formation.project.entities.TypeOperation;
import com.excilys.formation.project.repository.OperationDAO;

/**
 * Implémentation simple de {@link OperationDAO}.
 * 
 * @author excilys
 * 
 */
@Repository
public class OperationDAOImpl implements OperationDAO {

    /**
     * Entity Manager injecté par Spring.
     */
    @PersistenceContext
    private EntityManager em;

    @Override
    public List<Operation> getOperationsByCompteAndTimeIntervalAndType(String compteNumero, Interval interval, TypeOperation type) {
        return getOperationsByCompteAndTimeIntervalDependingOnType(compteNumero, interval, type, true);
    }

    @Override
    public List<Operation> getOperationsByCompteAndTimeIntervalExceptType(String compteNumero, Interval interval, TypeOperation type) {
        return getOperationsByCompteAndTimeIntervalDependingOnType(compteNumero, interval, type, false);
    }

    @Override
    public double getSumOperationsByCompteAndTimeIntervalAndType(String compteNumero, Interval interval, TypeOperation type) {
        Object[] args = { compteNumero, interval, type };
        noNullElements(args, "Aucun argument ne doit être null");
        StringBuilder jpqlQSB = new StringBuilder("SELECT SUM(o.montant) FROM Operation o INNER JOIN o.transaction t INNER JOIN o.compte c ");
        jpqlQSB.append("WHERE c.numero=:numero ");
        jpqlQSB.append("AND o.type=:type ");
        jpqlQSB.append("AND t.date BETWEEN :date_debut AND :date_fin ");
        jpqlQSB.append("GROUP BY o.type");

        Query q = em.createQuery(jpqlQSB.toString());
        q.setParameter("numero", compteNumero);
        if (type != null) {
            q.setParameter("type", type);
        }
        q.setParameter("date_debut", interval.getStart());
        q.setParameter("date_fin", interval.getEnd());
        if (q.getResultList().size() != 0) {
            return (Double) q.getSingleResult();
        } else {
            return 0;
        }
    }

    /**
     * Renvoie les opérations d'un compte donné dans un interval de temps à l'exception des opérations d'un certain type.
     * 
     * @param compteNumero
     *            Le numéro du compte dont il faut récupérer les opérations
     * @param interval
     *            Interval de dates pour récupérer les opérations
     * @param type
     *            Type d'opérations à inclure ou exclure
     * @param inclure
     *            S'il faut inclure (true) ou exclure (false) le type d'opérations choisi
     * 
     * @return les opérations d'un compte donné.
     */
    @SuppressWarnings("unchecked")
    private List<Operation> getOperationsByCompteAndTimeIntervalDependingOnType(String compteNumero, Interval interval, TypeOperation type,
            boolean inclure) {
        Object[] args = { compteNumero, interval };
        noNullElements(args, "compteNumero et interval ne peuvent être null");

        StringBuilder jpqlQSB = new StringBuilder("SELECT o FROM Operation o INNER JOIN o.transaction t ");
        jpqlQSB.append("WHERE o.compte.numero=:numero ");
        if (type != null) {
            if (inclure) {
                jpqlQSB.append("AND o.type=:type ");
            } else {
                jpqlQSB.append("AND o.type!=:type ");
            }
        }
        jpqlQSB.append("AND t.date BETWEEN :date_debut AND :date_fin ");
        jpqlQSB.append("ORDER BY t.date ");

        Query q = em.createQuery(jpqlQSB.toString());
        q.setParameter("numero", compteNumero);
        if (type != null) {
            q.setParameter("type", type);
        }
        q.setParameter("date_debut", interval.getStart());
        q.setParameter("date_fin", interval.getEnd());
        return q.getResultList();

    }

    @Override
    public void save(Operation operation) {
        em.persist(em.merge(operation));
    }

    @Override
    public double getSumEncoursByCompte(String compteNumero) {
        Object[] args = { compteNumero };
        noNullElements(args, "compteNumero ne peut être null");

        StringBuilder jpqlQSB = new StringBuilder("SELECT SUM(o.montant) FROM Operation o INNER JOIN o.transaction t INNER JOIN o.compte c ");
        jpqlQSB.append("WHERE c.numero=:numero ");
        jpqlQSB.append("AND t.dateEffet > t.date ");
        jpqlQSB.append("GROUP BY c.numero");

        Query q = em.createQuery(jpqlQSB.toString());
        q.setParameter("numero", compteNumero);
        if (q.getResultList().size() != 0) {
            return (Double) q.getSingleResult();
        } else {
            return 0;
        }

    }

    @Override
    public Map<String, Double> getSumEncoursByClient(long clientId) {
        Object[] args = { clientId };
        noNullElements(args, "clientId ne peut être null");

        StringBuilder jpqlQSB = new StringBuilder(
                "SELECT c.numero, SUM(o.montant) FROM Operation o INNER JOIN o.transaction t INNER JOIN o.compte c ");
        jpqlQSB.append("WHERE c.client.id=:clientId ");
        jpqlQSB.append("AND t.dateEffet > t.date ");
        jpqlQSB.append("GROUP BY c.numero");

        Query q = em.createQuery(jpqlQSB.toString());
        q.setParameter("clientId", clientId);

        Map<String, Double> resultMap = new HashMap<String, Double>();

        for (Object object : q.getResultList()) {

            Object[] myArray = (Object[]) object;
            resultMap.put((String) myArray[0], (Double) myArray[1]);
        }

        return resultMap;
    }
}
